//*//
import {
    cclegacy,
    Color,
    Director,
    director,
    Game,
    game,
    gfx,
    Material,
    MeshBuffer,
    Node,
    renderer,
    resources,
    sp,
    Texture2D,
    UIRenderer
} from "cc";
import { EDITOR, JSB } from "cc/env";

let _uid = 0;
let _mat = -1;
let _texid = 0;
let _texCount = 0;
let _spineCache = [];

let _spineTexture: Texture2D = null;
let _spineBuffers: Float32Array = null;
const _texMaps: Map<number, number> = new Map();
const _materialMaps: Map<number, any> = new Map();

const _region = new gfx.BufferTextureCopy();
_region.texExtent.width = 128;
_region.texExtent.height = 128;
_region.texOffset.x = 0;
_region.texOffset.y = 0;

//@ts-ignore
Material.prototype.spineCache = [];
export const SpineBatch: any = {
    enable: false,
    parent: null,
    next: function () {
        _texMaps.clear();
        _texCount = 0;
        _texid = 0;
        _mat = -1;
    },
    reset: function () {
        this.next();
        _uid = 0;
    },
    active: function () {
        return this.enable && this.parent;
    },

    update: function () {
        if (!this.active()) return;
        // _spineTexture?.uploadData(_spineBuffers);
        for (const value of _materialMaps.values()) {
            if (value) {
                value.n = 0;
            }
        }

        if (_uid > 0 && _spineTexture) {
            //@ts-ignore
            const gfxDevice = _spineTexture._getGFXDevice();
            const gfxTexture = _spineTexture.getGFXTexture();

            const hight = ~~((_uid * 4 - 1) / 128) + 1;
            _region.texExtent.height = hight > 128 ? 128 : hight;
            gfxDevice.copyBuffersToTexture([_spineBuffers], gfxTexture, [_region]);
        }
    }
};

const _getMaterialForBlendAndTint = function (comp: any, key: any) {
    _mat = key; //保存当前matKey

    let caches = _materialMaps.get(key);
    if (caches === undefined) {
        caches = { mats: [], n: 0 };
        _materialMaps.set(key, caches);
    }

    const inc = caches.n++;
    const materialCache = caches.mats;
    let inst: any = materialCache[inc];
    if (inst !== undefined) {
        _spineCache = inst.spineCache;
        return inst;
    }

    const type = key & 0xf;
    const src = (key >> 4) & 0xf;
    const dst = (key >> 8) & 0xf;

    const material = SpineBatch.parent;

    inst = new renderer.MaterialInstance({ parent: material });

    materialCache[inc] = inst;

    inst.overridePipelineStates({
        blendState: {
            blendColor: Color.WHITE,
            targets: [
                {
                    blendEq: gfx.BlendOp.ADD,
                    blendAlphaEq: gfx.BlendOp.ADD,
                    blendSrc: src,
                    blendDst: dst,
                    blendSrcAlpha: src,
                    blendDstAlpha: dst
                }
            ]
        }
    });
    inst.recompileShaders({ TWO_COLORED: type === 1 ? true : false, USE_LOCAL: false });

    inst.setProperty("spineData", _spineTexture);

    _spineCache = inst["spineCache"] = [];

    inst.addRef();

    return inst;
};

//预加载多纹理材质
const loadMaterial = function () {
    if (SpineBatch.parent) return;
    resources.load("multSpines/Mult-spine", Material, (err, material) => {
        if (!err) {
            //material.passes[0].tryCompile();
            SpineBatch.parent = material;
            material.addRef();
        }
    });
};

const initSpineDatas = function () {
    SpineBatch.enable = false;

    const size = 128;
    const gfxDevice = director.root!.device;
    const hasFeatureFloatTexture =
        gfxDevice.getFormatFeatures(gfx.Format.RGBA32F) & gfx.FormatFeatureBit.SAMPLED_TEXTURE;

    if (hasFeatureFloatTexture) {
        SpineBatch.enable = true;

        _spineTexture = new Texture2D();
        _spineBuffers = new Float32Array(size * size * 4);
        _spineTexture.setFilters(Texture2D.Filter.NEAREST, Texture2D.Filter.NEAREST);
        _spineTexture.reset({ width: size, height: size, format: Texture2D.PixelFormat.RGBA32F, mipmapLevel: 0 });
        _spineTexture.uploadData(_spineBuffers);
    }
};

const injectSpine = function () {
    const Skeleton: any = sp.Skeleton.prototype;
    Skeleton.isMult = false;

    //强制关闭合批
    Object.defineProperty(Skeleton, "enableBatch", {
        get: function () {
            if (this.customMaterial) return this._enableBatch;
            return false;
        },
        set: function (value: boolean) {
            if (this.customMaterial) {
                if (value !== this._enableBatch) {
                    this._enableBatch = value;
                    this._updateBatch();
                }
            }
        }
    });

    //获取共享材质
    const getMaterialForBlendAndTint = Skeleton.getMaterialForBlendAndTint;
    Skeleton.getMaterialForBlendAndTint = function (src: any, dst: any, type: any) {
        if (!SpineBatch.active() || this.customMaterial) {
            this.isMult = false;
            return getMaterialForBlendAndTint.call(this, src, dst, type);
        }

        const material = type | (src << 4) | (dst << 8);
        this.isMult = true;

        return material;
    };

    //绘画数据处理
    const render = Skeleton._render;
    Skeleton._render = function (batcher: any): void {
        if (!this.isMult) return render.call(this, batcher);

        let indicesCount = 0;
        if (this.renderData && this._drawList.length > 0) {
            const rd = this.renderData;
            const chunk = rd.chunk;
            const accessor = chunk.vertexAccessor;
            const meshBuffer = rd.getMeshBuffer()!;
            const origin = meshBuffer.indexOffset;

            //@ts-ignore
            const floatStride = meshBuffer.vertexFormatBytes >> 2;
            const isMultDraw = this._drawList.length > 1;
            const offset = chunk.vertexOffset;
            let length = rd.vertexCount;
            const vbuff = rd.chunk.vb;
            const ibuff = rd.indices;
            let isCommit = false;

            // Fill index buffer
            for (let i = 0; i < this._drawList.length; i++) {
                const dc = this._drawList.data[i];
                if (dc.texture) {
                    batcher.commitMiddleware(
                        this,
                        meshBuffer,
                        origin + dc.indexOffset,
                        dc.indexCount,
                        dc.texture,
                        dc.material,
                        this._enableBatch
                    );
                    isCommit = true;

                    if (isMultDraw) {
                        let j = dc.indexOffset;
                        length = dc.indexCount;
                        const id = _uid * 10 + _texid;
                        while (length--) {
                            vbuff[(ibuff[j++] - offset) * floatStride + 2] = id; //-offset
                        }
                    }
                }
                indicesCount += dc.indexCount;
            }

            if (!isMultDraw && isCommit) {
                const id = _uid * 10 + _texid;
                for (let i = 0, j = 0; i < length; i++) {
                    vbuff[j + 2] = id;
                    j += floatStride;
                }
            }

            if (isCommit) {
                const i = 16 * _uid++;
                const datas = _spineBuffers;
                const m = this.node.worldMatrix;
                datas[i] = m.m00;
                datas[i + 1] = m.m01;
                datas[i + 2] = m.m02;
                datas[i + 3] = m.m03;
                datas[i + 4] = m.m04;
                datas[i + 5] = m.m05;
                datas[i + 6] = m.m06;
                datas[i + 7] = m.m07;
                datas[i + 8] = m.m08;
                datas[i + 9] = m.m09;
                datas[i + 10] = m.m10;
                datas[i + 11] = m.m11;
                datas[i + 12] = m.m12;
                datas[i + 13] = m.m13;
                datas[i + 14] = m.m14;
                datas[i + 15] = m.m15;
            }
            const subIndices = rd.indices!.subarray(0, indicesCount);
            accessor.appendIndices(chunk.bufferId, subIndices);
            accessor.getMeshBuffer(chunk.bufferId).setDirty();
        }
    };
};

//多纹理合批流程
const injectBatch = function () {
    const Batcher2D: any = cclegacy.internal.Batcher2D.prototype;
    const commitMiddleware = Batcher2D.commitMiddleware;
    Batcher2D.commitMiddleware = function (
        comp: UIRenderer,
        meshBuffer: MeshBuffer,
        indexOffset: number,
        indexCount: number,
        tex: any,
        mat: any,
        enableBatch: boolean
    ) {
        //@ts-ignore
        if (!comp.isMult) {
            return commitMiddleware.call(this, comp, meshBuffer, indexOffset, indexCount, tex, mat, enableBatch);
        }

        // check if need merge draw batch
        const texture = tex.getGFXTexture();

        const tid = texture.objectID;
        _texid = _texMaps.get(tid);
        const isFlush = _texid === undefined && _texCount >= 8;

        if (
            !isFlush &&
            this._middlewareBuffer === meshBuffer &&
            _mat === mat && //this._currMaterial.hash === mat.hash
            this._middlewareIndexStart + this._middlewareIndexCount === indexOffset &&
            this._currLayer === comp.node.layer
        ) {
            this._middlewareIndexCount += indexCount;
        } else {
            this.autoMergeBatches(this._currComponent!);
            this.resetRenderStates();

            this._currComponent = comp;
            this._currTexture = texture;
            this._currSampler = tex.getGFXSampler();
            this._currTextureHash = tex.getHash();
            this._currLayer = comp.node.layer;
            this._currSamplerHash = this._currSampler.hash;
            this._currHash = 0;
            this._currTransform = null; //enableBatch ? null : comp.node;

            this._middlewareEnableBatch = false; //enableBatch;
            this._middlewareBuffer = meshBuffer;
            this._middlewareIndexStart = indexOffset;
            this._middlewareIndexCount = indexCount;

            this._currMaterial = _getMaterialForBlendAndTint(comp, mat);
        }

        this._currIsMiddleware = true;
        if (_texCount === 0 || _texid === undefined) {
            _texid = _texCount++;
            _texMaps.set(tid, _texid);
        }

        if (_spineCache[_texid] !== tid) {
            _spineCache[_texid] = tid;
            this._currMaterial.setProperty("texture" + _texid, tex);
        }
    };

    const mergeBatchesForMiddleware = Batcher2D.mergeBatchesForMiddleware;
    Batcher2D.mergeBatchesForMiddleware = function (renderComp: UIRenderer) {
        if (this._currIsMiddleware) SpineBatch.next();
        mergeBatchesForMiddleware.call(this, renderComp);
    };
};

//游戏启动前，务必加载多纹理材质
game.once(Game.EVENT_GAME_INITED, () => {
    if (EDITOR || JSB || !sp.Skeleton) return;
    initSpineDatas();
    if (SpineBatch.enable) {
        loadMaterial();
        injectSpine();
        injectBatch();
    }
});

director.on(Director.EVENT_AFTER_DRAW, dt => {
    SpineBatch.reset();
});
director.on(Director.EVENT_BEFORE_COMMIT, dt => {
    SpineBatch.update();
});

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//以下代码，仅供测试demo使用，请不要在项目中使用
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const changeBatch = function (node: Node, batch: boolean) {
    if (!node) return;
    const skeleton = node.getComponent(sp.Skeleton);
    if (skeleton) {
        skeleton.enableBatch = batch;
        skeleton.material = null;
        //@ts-ignore
        skeleton._updateBatch();
    }

    const childs = node.children;
    for (let i = 0; i < childs.length; i++) {
        changeBatch(childs[i], batch);
    }
};

export const switchMultSpine = function (useMult: boolean, useBatch: boolean) {
    SpineBatch.enable = useMult;
    const Skeleton: any = sp.Skeleton.prototype;

    if (!useMult) {
        Object.defineProperty(Skeleton, "enableBatch", {
            get: function () {
                return this._enableBatch;
            },
            set: function (value: boolean) {
                if (value !== this._enableBatch) {
                    this._enableBatch = value;
                    this._updateBatch();
                }
            }
        });
    } else {
        Object.defineProperty(Skeleton, "enableBatch", {
            get: function () {
                if (this.customMaterial) return this._enableBatch;
                return false;
            },
            set: function (value: boolean) {
                if (this.customMaterial) {
                    if (value !== this._enableBatch) {
                        this._enableBatch = value;
                        this._updateBatch();
                    }
                }
            }
        });
    }

    const scene = director.getScene() as Node;
    if (scene) {
        changeBatch(scene, useBatch);
    }
};

// */
